﻿<!DOCTYPE html>
<html>
<head>
  <title>Seating Chart</title>
  <!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
  <meta name="description" content="A seating chart editor for assigning places at tables." />
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <style>
    /* Use a Flexbox to make the Palette/Diagram responsive */
    #myFlexDiv {
      display: -webkit-flex;
      display: flex;
      flex-flow: row wrap;
      width: 100%;
      justify-content: center;
    }

    @media (min-width: 768px) {
      #myGuests {
        width: 100px;
        height: 500px;
        margin-right: 10px;
      }

      #myDiagramDiv {
        height: 500px;
        flex: 1;
      }
    }

    @media (max-width: 767px) {
      #myGuests {
        width: 90%;
        height: 100px;
        margin-bottom: 10px;
      }

      #myDiagramDiv {
        width: 90%;
        height: 500px;
      }
    }
  </style>
  <script src="../release/go.js"></script>
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
      var $ = go.GraphObject.make;

      // Initialize the main Diagram
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            allowDragOut: true,  // to myGuests
            allowClipboard: false,
            draggingTool: $(SpecialDraggingTool),
            rotatingTool: $(HorizontalTextRotatingTool),
            // For this sample, automatically show the state of the diagram's model on the page
            "ModelChanged": function(e) {
              if (e.isTransactionFinished) {
                document.getElementById("savedModel").textContent = myDiagram.model.toJson();
              }
            },
            "undoManager.isEnabled": true
          });

      myDiagram.nodeTemplateMap.add("",  // default template, for people
        $(go.Node, "Auto",
          { background: "transparent" },  // in front of all Tables
          // when selected is in foreground layer
          new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
          { locationSpot: go.Spot.Center },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          new go.Binding("text", "key"),
          { // what to do when a drag-over or a drag-drop occurs on a Node representing a table
            mouseDragEnter: function(e, node, prev) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;  // could be copied from palette
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true);
            },
            mouseDragLeave: function(e, node, next) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false);
            },
            mouseDrop: function(e, node) {
              assignPeopleToSeats(node, node.diagram.selection, e.documentPoint);
            }
          },
          $(go.Shape, "Rectangle", { fill: "blanchedalmond", stroke: null }),
          $(go.Panel, "Viewbox",
            { desiredSize: new go.Size(50, 38) },
            $(go.TextBlock, { margin: 2, desiredSize: new go.Size(55, NaN), font: "8pt Verdana, sans-serif", textAlign: "center", stroke: "darkblue" },
              new go.Binding("text", "", function(data) {
                var s = data.key;
                if (data.plus) s += " +" + data.plus.toString();
                return s;
              }))
          )
        ));

      // Create a seat element at a particular alignment relative to the table.
      function Seat(number, align, focus) {
        if (typeof align === 'string') align = go.Spot.parse(align);
        if (!align || !align.isSpot()) align = go.Spot.Right;
        if (typeof focus === 'string') focus = go.Spot.parse(focus);
        if (!focus || !focus.isSpot()) focus = align.opposite();
        return $(go.Panel, "Spot",
          { name: number.toString(), alignment: align, alignmentFocus: focus },
          $(go.Shape, "Circle",
            { name: "SEATSHAPE", desiredSize: new go.Size(40, 40), fill: "burlywood", stroke: "white", strokeWidth: 2 },
            new go.Binding("fill")),
          $(go.TextBlock, number.toString(),
            { font: "10pt Verdana, sans-serif" },
            new go.Binding("angle", "angle", function(n) { return -n; }))
        );
      }

      function tableStyle() {
        return [
          { background: "transparent" },
          { layerName: "Background" },  // behind all Persons
          { locationSpot: go.Spot.Center, locationObjectName: "TABLESHAPE" },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          { rotatable: true },
          new go.Binding("angle").makeTwoWay(),
          { // what to do when a drag-over or a drag-drop occurs on a Node representing a table
            mouseDragEnter: function(e, node, prev) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;  // could be copied from palette
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true);
            },
            mouseDragLeave: function(e, node, next) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false);
            },
            mouseDrop: function(e, node) { assignPeopleToSeats(node, node.diagram.selection, e.documentPoint); }
          }
        ];
      }

      // various kinds of tables:

      myDiagram.nodeTemplateMap.add("TableR8",  // rectangular with 8 seats
        $(go.Node, "Spot", tableStyle(),
          $(go.Panel, "Spot",
            $(go.Shape, "Rectangle",
              { name: "TABLESHAPE", desiredSize: new go.Size(160, 80), fill: "burlywood", stroke: null },
              new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
              new go.Binding("fill")),
            $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" },
              new go.Binding("text", "name").makeTwoWay(),
              new go.Binding("angle", "angle", function(n) { return -n; }))
          ),
          Seat(1, "0.2 0", "0.5 1"),
          Seat(2, "0.5 0", "0.5 1"),
          Seat(3, "0.8 0", "0.5 1"),
          Seat(4, "1 0.5", "0 0.5"),
          Seat(5, "0.8 1", "0.5 0"),
          Seat(6, "0.5 1", "0.5 0"),
          Seat(7, "0.2 1", "0.5 0"),
          Seat(8, "0 0.5", "1 0.5")
        ));

      myDiagram.nodeTemplateMap.add("TableR3",  // rectangular with 3 seats in a line
        $(go.Node, "Spot", tableStyle(),
          $(go.Panel, "Spot",
            $(go.Shape, "Rectangle",
              { name: "TABLESHAPE", desiredSize: new go.Size(160, 60), fill: "burlywood", stroke: null },
              new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
              new go.Binding("fill")),
            $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" },
              new go.Binding("text", "name").makeTwoWay(),
              new go.Binding("angle", "angle", function(n) { return -n; }))
          ),
          Seat(1, "0.2 0", "0.5 1"),
          Seat(2, "0.5 0", "0.5 1"),
          Seat(3, "0.8 0", "0.5 1")
        ));

      myDiagram.nodeTemplateMap.add("TableC8",  // circular with 8 seats
        $(go.Node, "Spot", tableStyle(),
          $(go.Panel, "Spot",
            $(go.Shape, "Circle",
              { name: "TABLESHAPE", desiredSize: new go.Size(120, 120), fill: "burlywood", stroke: null },
              new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
              new go.Binding("fill")),
            $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" },
              new go.Binding("text", "name").makeTwoWay(),
              new go.Binding("angle", "angle", function(n) { return -n; }))
          ),
          Seat(1, "0.50 0", "0.5 1"),
          Seat(2, "0.85 0.15", "0.15 0.85"),
          Seat(3, "1 0.5", "0 0.5"),
          Seat(4, "0.85 0.85", "0.15 0.15"),
          Seat(5, "0.50 1", "0.5 0"),
          Seat(6, "0.15 0.85", "0.85 0.15"),
          Seat(7, "0 0.5", "1 0.5"),
          Seat(8, "0.15 0.15", "0.85 0.85")
        ));

      // what to do when a drag-drop occurs in the Diagram's background
      myDiagram.mouseDrop = function(e) {
        // when the selection is dropped in the diagram's background,
        // make sure the selected people no longer belong to any table
        e.diagram.selection.each(function(n) {
          if (isPerson(n)) unassignSeat(n.data);
        });
      };

      // to simulate a "move" from the Palette, the source Node must be deleted.
      myDiagram.addDiagramListener("ExternalObjectsDropped", function(e) {
        // if any Tables were dropped, don't delete from myGuests
        if (!e.subject.any(isTable)) {
          myGuests.commandHandler.deleteSelection();
        }
      });

      // put deleted people back in the myGuests diagram
      myDiagram.addDiagramListener("SelectionDeleted", function(e) {
        // no-op if deleted by myGuests' ExternalObjectsDropped listener
        if (myDiagram.disableSelectionDeleted) return;
        // e.subject is the myDiagram.selection collection
        e.subject.each(function(n) {
          if (isPerson(n)) {
            myGuests.model.addNodeData(myGuests.model.copyNodeData(n.data));
          }
        });
      });

      // create some initial tables
      myDiagram.model = new go.GraphLinksModel([
        { "key": 1, "category": "TableR3", "name": "Head 1", "guests": {}, "loc": "143.5 58" },
        { "key": 2, "category": "TableR3", "name": "Head 2", "guests": {}, "loc": "324.5 58" },
        { "key": 3, "category": "TableR8", "name": "3", "guests": {}, "loc": "121.5 203.5" },
        { "key": 4, "category": "TableC8", "name": "4", "guests": {}, "loc": "364.5 223.5" }
      ]);  // this sample does not make use of any links


      // initialize the Palette
      myGuests =
        $(go.Diagram, "myGuests",
          {
            layout: $(go.GridLayout,
              {
                sorting: go.GridLayout.Ascending  // sort by Node.text value
              }),
            allowDragOut: true,  // to myDiagram
            allowMove: false
          });

      myGuests.nodeTemplateMap = myDiagram.nodeTemplateMap;

      // specify the contents of the Palette
      myGuests.model = new go.GraphLinksModel([
        { key: "Tyrion Lannister" },
        { key: "Daenerys Targaryen", plus: 3 },  // dragons, of course
        { key: "Jon Snow" },
        { key: "Stannis Baratheon" },
        { key: "Arya Stark" },
        { key: "Jorah Mormont" },
        { key: "Sandor Clegane" },
        { key: "Joffrey Baratheon" },
        { key: "Brienne of Tarth" },
        { key: "Hodor" }
      ]);

      myGuests.model.undoManager = myDiagram.model.undoManager  // shared UndoManager!

      // To simulate a "move" from the Diagram back to the Palette, the source Node must be deleted.
      myGuests.addDiagramListener("ExternalObjectsDropped", function(e) {
        // e.subject is the myGuests.selection collection
        // if the user dragged a Table to the myGuests diagram, cancel the drag
        if (e.subject.any(isTable)) {
          myDiagram.currentTool.doCancel();
          myGuests.currentTool.doCancel();
          return;
        }
        myDiagram.selection.each(function(n) {
          if (isPerson(n)) unassignSeat(n.data);
        });
        myDiagram.disableSelectionDeleted = true;
        myDiagram.commandHandler.deleteSelection();
        myDiagram.disableSelectionDeleted = false;
        myGuests.selection.each(function(n) {
          if (isPerson(n)) unassignSeat(n.data);
        });
      });

      go.AnimationManager.defineAnimationEffect("location",
        function(obj, startValue, endValue, easing, currentTime, duration, animationState) {
          obj.location = new go.Point(easing(currentTime, startValue.x, endValue.x - startValue.x, duration),
                                      easing(currentTime, startValue.y, endValue.y - startValue.y, duration));
        }
      );
    } // end init

    function isPerson(n) { return n !== null && n.category === ""; }

    function isTable(n) { return n !== null && n.category !== ""; }

    // Highlight the empty and occupied seats at a "Table" Node
    function highlightSeats(node, coll, show) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      var it = coll.iterator;
      while (it.next()) {
        var n = it.key;
        // if dragging a Table, don't do any highlighting
        if (isTable(n)) return;
      }
      var guests = node.data.guests;
      for (var sit = node.elements; sit.next();) {
        var seat = sit.value;
        if (seat.name) {
          var num = parseFloat(seat.name);
          if (isNaN(num)) continue;
          var seatshape = seat.findObject("SEATSHAPE");
          if (!seatshape) continue;
          if (show) {
            if (guests[seat.name]) {
              seatshape.stroke = "red";
            } else {
              seatshape.stroke = "green";
            }
          } else {
            seatshape.stroke = "white";
          }
        }
      }
    }

    // Given a "Table" Node, assign seats for all of the people in the given collection of Nodes;
    // the optional Point argument indicates where the collection of people may have been dropped.
    function assignPeopleToSeats(node, coll, pt) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      if (coll.any(isTable)) {
        // if dragging a Table, don't allow it to be dropped onto another table
        myDiagram.currentTool.doCancel();
        return;
      }
      // OK -- all Nodes are people, call assignSeat on each person data
      coll.each(function(n) { assignSeat(node, n.data, pt); });
      positionPeopleAtSeats(node);
    }

    // Given a "Table" Node, assign one guest data to a seat at that table.
    // Also handles cases where the guest represents multiple people, because guest.plus > 0.
    // This tries to assign the unoccupied seat that is closest to the given point in document coordinates.
    function assignSeat(node, guest, pt) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString());
      if (!(pt instanceof go.Point)) pt = node.location;

      // in case the guest used to be assigned to a different seat, perhaps at a different table
      unassignSeat(guest);

      var model = node.diagram.model;
      var guests = node.data.guests;
      // iterate over all seats in the Node to find one that is not occupied
      var closestseatname = findClosestUnoccupiedSeat(node, pt);
      if (closestseatname) {
        model.setDataProperty(guests, closestseatname, guest.key);
        model.setDataProperty(guest, "table", node.data.key);
        model.setDataProperty(guest, "seat", parseFloat(closestseatname));
      }

      var plus = guest.plus;
      if (plus) {  // represents several people
        // forget the "plus" info, since next we create N copies of the node/data
        guest.plus = undefined;
        model.updateTargetBindings(guest);
        for (var i = 0; i < plus; i++) {
          var copy = model.copyNodeData(guest);
          // don't copy the seat assignment of the first person
          copy.table = undefined;
          copy.seat = undefined;
          model.addNodeData(copy);
          assignSeat(node, copy, pt);
        }
      }
    }

    // Declare that the guest represented by the data is no longer assigned to a seat at a table.
    // If the guest had been at a table, the guest is removed from the table's list of guests.
    function unassignSeat(guest) {
      if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString());
      var model = myDiagram.model;
      // remove from any table that the guest is assigned to
      if (guest.table) {
        var table = model.findNodeDataForKey(guest.table);
        if (table) {
          var guests = table.guests;
          if (guests) model.setDataProperty(guests, guest.seat.toString(), undefined);
        }
      }
      model.setDataProperty(guest, "table", undefined);
      model.setDataProperty(guest, "seat", undefined);
    }

    // Find the name of the unoccupied seat that is closest to the given Point.
    // This returns null if no seat is available at this table.
    function findClosestUnoccupiedSeat(node, pt) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      var guests = node.data.guests;
      var closestseatname = null;
      var closestseatdist = Infinity;
      // iterate over all seats in the Node to find one that is not occupied
      for (var sit = node.elements; sit.next();) {
        var seat = sit.value;
        if (seat.name) {
          var num = parseFloat(seat.name);
          if (isNaN(num)) continue;  // not really a "seat"
          if (guests[seat.name]) continue;  // already assigned
          var seatloc = seat.getDocumentPoint(go.Spot.Center);
          var seatdist = seatloc.distanceSquaredPoint(pt);
          if (seatdist < closestseatdist) {
            closestseatdist = seatdist;
            closestseatname = seat.name;
          }
        }
      }
      return closestseatname;
    }

    // Position the nodes of all of the guests that are seated at this table
    // to be at their corresponding seat elements of the given "Table" Node.
    function positionPeopleAtSeats(node) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      var guests = node.data.guests;
      var model = node.diagram.model;
      for (var seatname in guests) {
        var guestkey = guests[seatname];
        var guestdata = model.findNodeDataForKey(guestkey);
        positionPersonAtSeat(guestdata);
      }
    }

    // Position a single guest Node to be at the location of the seat to which they are assigned.
    function positionPersonAtSeat(guest) {
      if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString());
      if (!guest || !guest.table || !guest.seat) return;
      var diagram = myDiagram;
      var table = diagram.findPartForKey(guest.table);
      var person = diagram.findPartForData(guest);
      if (table && person) {
        var seat = table.findObject(guest.seat.toString());
        var loc = seat.getDocumentPoint(go.Spot.Center);
        // animate movement, instead of: person.location = loc;
        var animation = new go.Animation();
        animation.add(person, "location", person.location, loc);
        animation.start();
      }
    }


    // Automatically drag people Nodes along with the table Node at which they are seated.
    function SpecialDraggingTool() {
      go.DraggingTool.call(this);
      this.isCopyEnabled = false;  // don't want to copy people except between Diagrams
    }
    go.Diagram.inherit(SpecialDraggingTool, go.DraggingTool);

    SpecialDraggingTool.prototype.computeEffectiveCollection = function(parts) {
      var map = go.DraggingTool.prototype.computeEffectiveCollection.call(this, parts);
      // for each Node representing a table, also drag all of the people seated at that table
      parts.each(function(table) {
        if (isPerson(table)) return;  // ignore persons
        // this is a table Node, find all people Nodes using the same table key
        for (var nit = table.diagram.nodes; nit.next();) {
          var n = nit.value;
          if (isPerson(n) && n.data.table === table.data.key) {
            if (!map.has(n)) map.add(n, new go.DraggingInfo(n.location.copy()));
          }
        }
      });
      return map;
    };
    // end SpecialDraggingTool


    // Automatically move seated people as a table is rotated, to keep them in their seats.
    // Note that because people are separate Nodes, rotating a table Node means the people Nodes
    // are not rotated, so their names (TextBlocks) remain horizontal.
    function HorizontalTextRotatingTool() {
      go.RotatingTool.call(this);
    }
    go.Diagram.inherit(HorizontalTextRotatingTool, go.RotatingTool);

    HorizontalTextRotatingTool.prototype.rotate = function(newangle) {
      go.RotatingTool.prototype.rotate.call(this, newangle);
      var node = this.adornedObject.part;
      positionPeopleAtSeats(node);
    };
    // end HorizontalTextRotatingTool
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myFlexDiv">
    <div id="myGuests" style="border: solid 1px black"></div>

    <div id="myDiagramDiv" style="border: solid 1px black"></div>
  </div>
  This sample demonstrates how a "Person" node can be dropped onto a "Table" node,
  causing the person to be assigned a position at the closest empty seat at that table.
  When a person is dropped into the background of the diagram, that person is no longer assigned a seat.
  People dragged between diagrams are automatically removed from the diagram they came from.
  <p>
  "Table" nodes are defined by separate templates, to permit maximum customization of the shapes and
  sizes and positions of the tables and chairs.
  <p>
  "Person" nodes in the <code>myGuests</code> diagram can also represent a group of people,
  for example a named person plus one whose name might not be known.
  When such a person is dropped onto a table, additional nodes are created in <code>myDiagram</code>.
  Those people are seated at the table if there is room.
  <p>
  Tables can be moved or rotated. Moving or rotating a table automatically repositions the people seated at that table.
  <p>
  The <a>UndoManager</a> is shared between the two Diagrams, so that one can undo/redo in either diagram
  and have it automatically handle drags between diagrams, as well as the usual changes within the diagram.
  </p>
  <div>
    Diagram Model saved in JSON format, automatically updated after each transaction:
    <pre id="savedModel" style="height:250px"></pre>
  </div>
</div>
</body>
</html>
